Enunciado¶

Bienvenidos a la Actividad 1, donde pondremos en práctica todo lo aprendido durante el bloque 2. Esta actividad la realizaremos en clase, se terminará en casa (debería completarse en clase) y se entregará el día 8 de octubre.

¿En qué consiste?¶

Vamos a poner en práctica cuatro aspectos del procesamiento de imágenes:

  • Lectura de imagen y conversión a escala de grises
  • Umbralización de una imagen siguiendo los métodos vistos en clase (y comparándolos)
  • Morfología matemática con los operadores conocidos
  • Detección y medida de objetos dentro de la imagen. Este último aspecto lo veremos más adelante en el curso, con lo cual, lo que haremos aquí será sencillo y guiado (pero requerirá un esfuerzo pequeño para enfrentaros a un problema que no habéis visto antes)

La finalidad es sencilla. Se os dará una imagen, a color, que tiene varias tonalidades y que está pintada con círculos.

La actividad consiste en contar el número de círculos de la imagen. image.png

Evaluación¶

Se evaluará de la siguiente manera:

  • 0 puntos si no se presenta o si sólo se presenta el enunciado con modificaciones mínimas o si el ejercicio no resuelve el problema.
  • 5 puntos si se presenta únicamente la resolución del ejercicio (en este caso un proceso que calcule el número de puntos), pero no se justifica los pasos realizados o no se comentan los resultados (comparación entre umbralizaciones, por ejemplo).
  • 5-10 puntos, dependiendo de los comentarios realizados y de la profundidad de las explicaciones.

Formato de entrega¶

  • Se pide entregar el ejercicio de dos maneras: -- En formato .py (con el código puro) -- En formato .html

No se aceptará el formato .ipynb Habilitaré una actividad en Canvas para que podáis subir ambos archivos.

Inicialización¶

En primer lugar, cargamos todos los paquetes/frameworks que nos van a hacer falta. Se recomienda visitar la web: https://scikit-image.org/ para ver todas las funcionalidades que permite Scikit Image.

In [1]:
# Paquetes necesarios para la realización de esta práctica (no son necesarios conocerlos ni entenderlos por ahora)
from skimage.io import imread
from skimage import transform as tf

import matplotlib.pyplot as plt

# Cargamos la función para convertir de RGB a Escala de grises
from skimage.color import rgb2gray
In [2]:
# Paquete y funciones para realizar una umbralización con Scikit-image
from skimage.filters import threshold_otsu, threshold_local, threshold_niblack, threshold_sauvola


# Paquetes necesarios para la morfología matemática
from skimage.morphology import erosion, dilation, opening, closing
# Elementos estructurales
from skimage.morphology import disk, diamond, ball, rectangle

# Estas dos funciones nos sirven para detectar los objetos dentro de una imagen binaria
from skimage.morphology import label
from skimage.measure import regionprops
In [3]:
# Defino una función para mostrar una imagen por pantalla con el criterio que considero más acertado
def imshow(img, title):
    fig, ax = plt.subplots(figsize=(7, 7))
    # El comando que realmente muestra la imagen
    ax.imshow(img,cmap=plt.cm.gray)
    # Para evitar que aparezcan los números en los ejes
    ax.set_xticks([]), ax.set_yticks([])
    ax.set_title(title)
    plt.show()

Cargar la imagen¶

Lo primero de todo, vamos a leer la imagen. Recuerda que hay que subir la imagen cada vez que inicies sesión en el notebook y que la ruta se mira haciendo botón derecho sobre el archivo.

Con lo cual, aquí vamos a hacer dos cosas:

  • Cargar la imagen
  • Convertirla a escala de grises

Hacemos esto para luego posteriormente umbralizar la imagen en escala de grises.

In [4]:
from google.colab import files
from PIL import Image
uploaded = files.upload() #subir archivo
imagenpuntos = list(uploaded.keys())[0] #damos un nombre al archivo
imagen = Image.open(imagenpuntos)
imagen.show()
Upload widget is only available when the cell has been executed in the current browser session. Please rerun this cell to enable.
Saving Pintura_Puntos.jpg to Pintura_Puntos.jpg
In [5]:
plt.imshow(imagen)
plt.axis('off')  #para ocultar los ejes
plt.show()
In [6]:
from skimage.color import rgb2gray
In [7]:
imagenpuntos_gris = rgb2gray(imagen) #mediante este comando lo paso a escala de grises
In [8]:
plt.imshow(imagenpuntos_gris, cmap="gray") #cuando uso la funcion plt se necesita indicar que mapee en gris"
plt.axis('off')
plt.show()

He cargado la imagen por medio del uso de la biblioteca PIL, tras esto he mostrado la imagen por pantalla con plt.show. Despues paso la imagen a escala de grises usando rgb2gray, y cunado lo muestro con plt lo mapeo en gris tambien.

In [9]:
# Os tendría que ir quedando una cosa así

Umbralizar la imagen con varios métodos¶

Vamos a probar ahora diferentes métodos para umbralizar la imagen. Se pide en esta actividad:

  • Ejecutar cada método (para ello tendréis que ver en la documentación cómo invocarlo y qué parámetros necesita)
  • Mostrar por pantalla el resultado de cada imagen, añadiendo una explicación de a qué corresponde cada imagen
  • Hacer una comparación general de todos (es decir, cuál da mejores resultados, aspectos que observeis, etc...)
  • Explicar a qué tipo corresponden los métodos de threshold_niblack y threshold_sauvola
  • Por último, ¿se obtiene el mismo resultado si se rota la imagen 180º?¿Por qué?

1.UMBRALIZACION GLOBAL CON MÉTODO DE OTSU

In [10]:
# Calcula el umbral de Otsu
umbral_otsu = threshold_otsu(imagenpuntos_gris)

# Aplica la umbralización de Otsu a la imagen
imagen_umbralizada = imagenpuntos_gris > umbral_otsu

# Muestra la imagen original y la imagen umbralizada
plt.figure(figsize=(10, 4))

plt.subplot(1, 2, 1)
plt.imshow(imagenpuntos_gris, cmap='gray')
plt.title('Imagen en Escala de Grises')
plt.axis('off')

plt.subplot(1, 2, 2)
plt.imshow(imagen_umbralizada, cmap='gray')
plt.title('Imagen Umbralizada con Otsu')
plt.axis('off')

plt.show()

Lo primero que hemos hecho ha sido lanzar el comando para que me determine el umbral que va a usar el metodo global con otsu. Una vez realizado esto transforma la imagen teniendo en cuenta el umbral establecido. Sacamos por pantalla dos imagenes que me comparen la imagen en escala de grises sin treshold y la otra imagen umbralizada con Otsu.

  1. TRESHOLD LOCAL
In [11]:
# Calcula el umbral local utilizando threshold_local
umbral_local = threshold_local(imagenpuntos_gris, block_size=53)

# Aplica la umbralización local a la imagen
imagen_umbralizada_local = imagenpuntos_gris > umbral_local

# Muestra la imagen original y la imagen umbralizada local
plt.figure(figsize=(10, 4))

plt.subplot(1, 2, 1)
plt.imshow(imagenpuntos_gris, cmap='gray')
plt.title('Imagen en Escala de Grises')
plt.axis('off')

plt.subplot(1, 2, 2)
plt.imshow(imagen_umbralizada_local, cmap='gray')
plt.title('Imagen Umbralizada con Umbral Local')
plt.axis('off')

plt.show()

He umbralizado usando 53 pixeles de alrededor para que la haga media con esa cantidad porque si cogo un numero muy grande me estaria cogiendo practicamente toda la imagen y eso no lo queremos. He ido problando hasta que con un blocksize de 53 se parece a la imagen que se decia que era la mejor que se podia obtener.

Este tipo de umbralizacion es UMBRALIZACION ADAPTATIVA

In [12]:
import numpy as np
In [13]:
import matplotlib.pyplot as plt
In [14]:
histograma = plt.hist(imagenpuntos_gris.ravel(), bins=256, range=(0, 1))

He sacado el histograma

  1. UMBRALIZACION CON threshold_niblack
In [15]:
umbral_niblack = threshold_niblack(imagenpuntos_gris, window_size=55)
imagen_umbralizada_niblack = imagenpuntos_gris > umbral_niblack #umbralizamos la imagen
In [16]:
plt.figure(figsize=(10, 4))
Out[16]:
<Figure size 1000x400 with 0 Axes>
<Figure size 1000x400 with 0 Axes>
In [17]:
plt.subplot(1, 2, 1)
plt.imshow(imagenpuntos_gris, cmap='gray')
plt.title('Imagen en Escala de Grises')
plt.axis('off')

plt.subplot(1, 2, 2)
plt.imshow(imagen_umbralizada_niblack, cmap='gray')
plt.title('Imagen Umbralizada con Niblack')
plt.axis('off')

plt.show()

He umbralizado con niblack que es un metodo ADAPTATIVO. He mostrado por pantalla la comparizacion de la imagen original en escala de grises frente a la umbralizada con niblack pudiendo observar como se ha converrtido la ia imagen en puntos blancos y negros (binaria).

  1. UMBRALIZACION CONTRESHOLD_SAUVOLA
In [18]:
umbral_sauvola = threshold_sauvola(imagenpuntos_gris, window_size=51)
imagen_umbralizada_sauvola = imagenpuntos_gris > umbral_sauvola #umbralizamos la imagen
plt.figure(figsize=(10, 4))
plt.subplot(1, 2, 1)
plt.imshow(imagenpuntos_gris, cmap='gray')
plt.title('Imagen en Escala de Grises')
plt.axis('off')

plt.subplot(1, 2, 2)
plt.imshow(imagen_umbralizada_niblack, cmap='gray')
plt.title('Imagen Umbralizada con Sauvola')
plt.axis('off')

plt.show()

He realizado la misma funcion de antes, pero he usado treshold_sauvola y he metido un windows size de 67. Este es un método ADAPTATIVO

Tenemos que rotar 180 grados la imagen que mejor deje definidos los circulos de la imagen original para asi poder contar de manera mas especifica y correcta. La imagen elegida es la que hemos aplicado el umbral local.

In [19]:
imagen_girada = np.rot90(np.rot90(imagenpuntos_gris))
In [20]:
plt.figure(figsize=(10, 4))
plt.subplot(1, 2, 2)
plt.imshow(imagen_girada, cmap="gray")
plt.title('Imagen Girada 180 Grados')
plt.axis('off')

plt.show()
In [21]:
# Calcula el umbral local utilizando threshold_local
umbral_local = threshold_local(imagen_girada, block_size=53)

# Aplica la umbralización local a la imagen
imagen_umbralizada_local_girada = imagen_girada > umbral_local

# Muestra la imagen original y la imagen umbralizada local
plt.figure(figsize=(10, 4))

plt.subplot(1, 2, 1)
plt.imshow(imagen_girada, cmap='gray')
plt.title('Imagen en Escala de Grises Girada')
plt.axis('off')

plt.subplot(1, 2, 2)
plt.imshow(imagen_umbralizada_local_girada, cmap='gray')
plt.title('Imagen Umbralizada con Umbral Local Girada')
plt.axis('off')

plt.show()

El proceso seguido ha sido girar la imagen en escala de grises y luego umbralizarla. Como no se si es lo mismo que girar directamente la umbralizada ahora voy a girar de manera directa:

In [22]:
imagen_girada2 = np.rot90(np.rot90(imagen_umbralizada_local))
In [23]:
plt.subplot(1, 2, 1)
plt.imshow(imagen_girada2, cmap='gray')
plt.title('Imagen Umbralizada con Umbral Local directamente Girada')
plt.axis('off')

plt.show()

Podemos observar que independientemente del orden en que hagamos la rotacion, si es antes de la umbralizacion o despues, se mantienen igual.

In [24]:
# Este es el mejor resultado que tendríais que alcanzar

Morfología Matemática¶

Como se puede apreciar en la imagen hay varios elementos imperfectos:

  • Hay círculos que están en contacto con otros círculos
  • Hay círculos que están huecos por centro
  • Hay círculos que no están cerrados del todo

Mediante el uso de morfología matemática (concretamente los cuatro operadores visto en clase) y los posibles elementos estructurales existentes, se pide:

  • Decidir qué elemento estructural, y por qué, es el más adecuado. Indicar también el tamaño del elemento estructural que se ha decidido escoger.
  • ¿Qué operador o secuencia de operadores elegiríais?
  • Una vez elegido operador y elemento estructural, repetir este mismo proceso (es decir, elegir operador y elemento estructural), para la imagen complementaria, ¿qué conclusiones sacas de esto?

Voy a probar con los 4 tipos de operadores matematicos (erosion, dilatacion, apertura y clausura). Despues los comapraré e intentaré sacar como conclusion con cual de los 4 metodos obtengo una imagen con mejor detalle de puntos para hacer un buen conteo de los mismos.

EROSION

In [25]:
import skimage.io
from skimage.morphology import erosion, dilation, opening, closing
from skimage import img_as_ubyte
In [26]:
kernel = np.ones((3, 3), np.uint8) # he leido que los kernels de menor tamaño pueden resaltar detalles mas finos. Considero que estamso ante un caso donde se necesita unalto grado de detalle y por esto mi kernel va a tener unos numeros bastante pequeños.
imagen_erosionada = erosion(imagen_umbralizada_local, kernel)
skimage.io.imshow(imagen_umbralizada_local)
skimage.io.imshow(imagen_erosionada)
skimage.io.show()

DILATACION

In [27]:
kernel = np.ones((1, 1), np.uint8) # he leido que los kernels de menor tamaño pueden resaltar detalles mas finos. Considero que estamso ante un caso donde se necesita unalto grado de detalle y por esto mi kernel va a tener unos numeros bastante pequeños.
imagen_dilation = dilation(imagen_umbralizada_local, kernel)
skimage.io.imshow(imagen_umbralizada_local)
skimage.io.imshow(imagen_dilation)
skimage.io.show()

la dilatacion me aumenta el tamaño de los circulos, por esto mismo he usado el tamaño de kernel mas pequeño posible ya que sino los circulos van a perder su controno y me quedare con una imagen completamente blanca.

APERTURA

In [28]:
kernel = np.ones((1, 1), np.uint8) # he leido que los kernels de menor tamaño pueden resaltar detalles mas finos. Considero que estamso ante un caso donde se necesita unalto grado de detalle y por esto mi kernel va a tener unos numeros bastante pequeños.
imagen_opening = opening(imagen_umbralizada_local, kernel)
skimage.io.imshow(imagen_umbralizada_local)
skimage.io.imshow(imagen_opening)
skimage.io.show()

Esta funcion primero erosiona y luego dilata, pero pienso que siguen quedando mejor definidos los puntos conla funcion de dilatacion a secas.

CLAUSURA

In [29]:
kernel = np.ones((1,1), np.uint8) # he leido que los kernels de menor tamaño pueden resaltar detalles mas finos. Considero que estamso ante un caso donde se necesita unalto grado de detalle y por esto mi kernel va a tener unos numeros bastante pequeños.
imagen_closing = closing(imagen_umbralizada_local, kernel)
skimage.io.imshow(imagen_umbralizada_local)
skimage.io.imshow(imagen_closing)
skimage.io.show()

Por medio de la apertura observo que la linea negra del centro no diferencia bien el tamaño de los circulos.

como conclusion pienso que el mejor operador morfologico matematico sobre esta imagen para que los putnos queden bien definidos en una dilatacion con el kernel de menor tamaño posible, de tal manera que elcontrono de las circunferencias no desaparezcan pero nos delimite de la mejor manera posible la linea negra central donde los contronos son mas gruesos.

Contando círculos¶

Haciendo uso de las funcionalidades cargadas al principio, se pide hacer una función que:

  • Reciba como parámetro una imagen binaria
  • Compruebe que es binaria y si no es binaria, deberá imprimir por pantalla que no es binaria
  • Cuente el número de círculos dentro de la imagen.
  • Devuelva (return) dicho número de círculos.

Por último, ¿qué se podría hacer para asegurar que no se tienen en cuenta posibles errores en la umbralización como pequeños puntos o posible ruido que haya llegado hasta este punto?

In [30]:
 imagen_binaria = imagen_dilation.astype(np.uint8)
 print
(imagen_binaria)
Out[30]:
array([[1, 1, 1, ..., 1, 1, 1],
       [1, 1, 1, ..., 0, 0, 0],
       [1, 1, 1, ..., 0, 0, 0],
       ...,
       [1, 1, 1, ..., 1, 1, 1],
       [1, 1, 1, ..., 1, 1, 1],
       [1, 1, 1, ..., 1, 1, 1]], dtype=uint8)
In [37]:
len(regionprops(label(imagen_dilation))) #aqui obtengo la cantidad de circulos que me detecta usando labels y regionprops.
Out[37]:
5680
In [32]:
print(imagen_binaria)
[[1 1 1 ... 1 1 1]
 [1 1 1 ... 0 0 0]
 [1 1 1 ... 0 0 0]
 ...
 [1 1 1 ... 1 1 1]
 [1 1 1 ... 1 1 1]
 [1 1 1 ... 1 1 1]]
In [35]:
plt.imshow(imagen_dilation, 'gray')
Out[35]:
<matplotlib.image.AxesImage at 0x78432212bee0>

(Bonus) Automatizamos el proceso de extracción¶

Esta sección no es obligatoria pero la pongo para aquellos que quieran saber "¿y ahora qué se haría?".

Lo que hemos hecho hasta ahora es:

  • Encontrar un método de umbralización adecuado (que tiene unos parámetros que ya hemos fijado - según nuestro criterio)
  • Hemos utilizado unos métodos de morfología matemática, también con sus parámetros

Es decir, tenemos varios parámetros y tenemos una función que nos dice cuál es el número de puntos dada una imagen. Variando dichos parámetros, variará también el número de puntos, pero no parece haber una relación directa.

También no hay que olvidar que desconocemos el número de puntos (nunca se ha dicho, aunque siempre puedes contarlos), por lo que no podemos seguir un proceso de aprendizaje supervisado (tipo descenso del gradiente sobre los parámetros anteriores para encontrar el mejor resultado).

Pero lo que sí podemos hacer es iterar el valor de los parámetros para alcanzar un máximo de puntos (asumiendo que dicho máximo corresponderá con el mejor resultado). Esto suele hacerse cuando no sabemos exáctamente el resultado que esperamos.

En definitiva, ahora se buscaría realizar un proceso iterativo para encontrar el valor máximo del número de puntos. Para ello haría falta:

  • Crear una función que englobe todos los procesos anteriores (umbralización, morfología matemática, etc...) y que tome como parámetro de entrada una imagen en color y devuelva el número de puntos.
  • Crear los intervalos donde variarán todos los parámetros que queremos ir modificando
  • Crear un proceso iterativo que vaya recorriendo todas las combinaciones de elementos (se recomienda el uso de product del paquete itertools).
  • Una vez terminado el proceso quedarnos con la combinación de paráteros ganadora y el resultado del número de puntos.

Podría decirse que esa combinación de parámetros es la mejor.

In [40]:
from skimage import io, color, measure, morphology, filters
import itertools

def contar_puntos(imagen, umbral=None, tamaño_minimo=None):
    # Realiza todas las operaciones necesarias en la imagen
    imagen_gris = color.rgb2gray(imagen)

    if umbral is None:
        umbral = filters.threshold_otsu(imagen_gris)

    imagen_binaria = imagen_gris > umbral

    if tamaño_minimo is None:
        tamaño_minimo = 10

    imagen_morfologia = morphology.remove_small_objects(imagen_binaria, min_size=tamaño_minimo)
    etiquetas = measure.label(imagen_morfologia, background=0)
    regiones = measure.regionprops(etiquetas)
    return len(regiones)
   # Define los intervalos de parámetros que deseas explorar
intervalo_umbral = [0.1, 0.2, 0.3]
intervalo_tamaño_minimo = [5, 10, 20]

mejor_combinacion = None
max_puntos = 0

# Genera todas las combinaciones posibles de parámetros
for umbral, tamaño_minimo in itertools.product(intervalo_umbral, intervalo_tamaño_minimo):
    # Aplica la función contar_puntos con la combinación de parámetros actual
    puntos = contar_puntos(imagen, umbral, tamaño_minimo)

    # Actualiza el número máximo de puntos y la mejor combinación de parámetros
    if puntos > max_puntos:
        max_puntos = puntos
        mejor_combinacion = (umbral, tamaño_minimo)

print(f"Mejor combinación de parámetros: Umbral = {mejor_combinacion[0]}, Tamaño mínimo = {mejor_combinacion[1]}")
print(f"Número máximo de puntos detectados: {max_puntos}")
Mejor combinación de parámetros: Umbral = 0.2, Tamaño mínimo = 5
Número máximo de puntos detectados: 2503